#!/usr/bin/python3

# Copyright (c) 2011 Michael Mol <mikemol@gmail.com>
#
# This work is licensed under a Creative Commons Attribution 3.0 Unported License, viewable at
# http://creativecommons.org/licenses/by/3.0/
#

#------------- Look beyond the curtain --------------#
#                                                    #
#  Pay no attention to the man behind the curtain    #
# You'll be asked to read this code once you've read #
#    the rest of the docs below. Come back later.    #
#                                                    #
#------------- Look beyond the curtain --------------#

def chainAppend(chain, filterObj):
    chain.append(filterObj)
    return chain

def chainBuild(filterObj):
    # Initialize a list of generators.
    return [filterObj]

def iterableBase(indata):
    for item in indata:
        yield item

def chainRun(chain, inputdata):
    # Build an iterable object from the chain spec.
    obj = iterableBase(inputdata)
    for filterObj in chain:
        # Stack on another 
        obj = filterObj(obj)
    return obj

#---------- Here's the curtain. See below ----------

#--
# What is it?
#--
#
# This module is designed build filter chains using Python generator objects.
# 
# A filter chain is system where data is entered on one end, passed through a
# series of filters, and comes out the other end. Each filter data passes
# has has an opportunity to modify the data stream.

#--
# How does it work?
#--
#
# In this system, each filter is a generator object. Each generator object
# retrieves data from an iterable object passed to it, usually a generator. The
# first generator object in the chain is given raw data, and the last generator
# object in the chain is used directly by the consumer of this object.

#--
# Filter/data compatibility
#--
#
# As the user of this module, you specify the filters which go in the chain. As
# such, you should be sure to choose (or create) filters which can safely
# operate on the data you chose to provide. Otherwise, data/filter-specific
# exceptions are likely to be thrown. For example, if you have a filter which
# converts strings to lower-case, and you give it a database object which
# doesn't behave like a string, you're sure to see an exception. However, just
# because you try it, and it doesn't crash, that doesn't mean all is well; you
# may have passed an object which implements the 'upper' method, but for which
# that method doesn't do anything analogous to string's.

# All in all, be smart about the filters you use and the data you pass. This
# isn't magic. You are expected to know what you're doing, or be responsible if
# you don't.

#--
# Simple filter example
#--
#
# You are probably going to need to write your own filters to do some of what
# you want to do. Here is a simple filter which converts strings to lower case

def filterToLower(indata):
    for item in indata:
        yield item.lower()

#--
# Adding or removing data
#--
#
# There's no requirement that the amount of data passed in is equal to the
# amount of data passed out. You can remove elements, as in this example which
# removes any instance of None from the data:

def filterRemoveNone(indata):
    for item in indata:
        if item:
            yield item

# You can also add data, as in this example:

def filterExParrot(indata):
    for item in indata:
        if item == "parrot":
            yield "EX"
            yield item
        else:
            yield item

#--
# Building a chain
#--
#
# So now you know how to *build* filters, but how would you build a chain?
#
# Call chainBuild, and pass it the generator object representing a filter
# you want in the chain. Here, we're going to build a filter chain which 
# will convert its input to lower-case.

if __name__ == '__main__':
    chain = chainBuild(filterToLower)

# That's great, but now we need it to actually operate on something. To get
# an iterable object for a chain, call chainRun, pass it the chain object, and
# pass it the data you want it to operate on as an iterable object (meaning a
# tuple, a list, another generator (such as a database recordset, another
# chain, whatever you like!)

if __name__ == '__main__':
    chain = chainBuild(filterToLower)
    inputdata = "This is a parrot".split()
    iterable = chainRun(chain, inputdata)
    for item in iterable:
        print( item, end=' ')
    print()

#--
# Adding filters to a chain
#--
# Sweet. But what if we don't want a parrot, but an EX parrot, and we still
# want the rest to be lower case? Call chainAppend and pass it the filter we
# want.

if __name__ == '__main__':
    chain = chainBuild(filterToLower)
    chain = chainAppend(chain, filterExParrot)
    inputdata = "This is a parrot".split()
    iterable = chainRun(chain, inputdata)
    for item in iterable:
        print( item, end=' ')
    print()

# Adding a second filter is easy, too. Let's say our input data is sprinkled
# with None objects, which we don't really need, and keep causing filterToLower
# to crash. We can use filterRemoveNone to clean up our input.

if __name__ == '__main__':
    chain = chainBuild(filterRemoveNone)
    chain = chainAppend(chain, filterToLower)
    chain = chainAppend(chain, filterExParrot)
    inputdata = "This is a parrot".split()
    inputdata2 = []
    for item in inputdata:
        inputdata2.append(None)
        inputdata2.append(item)
        inputdata2.append(None)
    iterable = chainRun(chain, inputdata2)
    for item in iterable:
        print( item, end=' ')
    print()

#--
# Final words and warnings
#--
# That's all there is to it. Now, you may be tempted to modify the 'chain'
# object directly. Don't. Its structure may change; I'm new to Python, and I
# may find a better way to do it. If you want to store the names of the chains
# you want to apply, in the order you want to apply them, that's fine; if you
# serialize the chain object yourself, you're on your own.
#
# Also, you'll probably notice the lack of a 'Prepend' method. That's
# intentional. Debugging something like this is likely to be a pain, and I'd
# rather prepends and positional inserts sprinkled throughout some code weren't
# a potential source of bugs. I'll add it if I must, but I'd rather not.
#
# FYI: once you call chainRun, the returned object from chainRun cannot be
# affected by further modifications of your chain.
#
# Finally, go back to that 'curtain' section near the top of the file, and read
# the implementations of the methods used here. Understand that this code is
# actually very simple, and you shouldn't think of it as magic.

